package de.hsb.ai.agent;

import game.Move;
import game.PlayGame;
import jade.content.ContentElement;
import jade.content.lang.Codec.CodecException;
import jade.content.onto.OntologyException;
import jade.content.onto.basic.Action;
import jade.content.onto.basic.Result;
import jade.core.AID;
import jade.core.Agent;
import jade.domain.introspection.EventRecord;
import jade.domain.introspection.Occurred;
import jade.lang.acl.ACLMessage;
import jade.proto.SimpleAchieveREInitiator;

import java.awt.Dimension;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import javax.swing.SwingUtilities;

import api.PlayerController;
import de.hsb.ai.domain.DummyAID;
import de.hsb.ai.domain.GameInfo;
import de.hsb.ai.domain.GameOver;
import de.hsb.ai.domain.GameStarted;
import de.hsb.ai.domain.MoveEvent;
import de.hsb.ai.domain.action.GetMove;
import de.hsb.ai.util.ScotlandJadeGameLogger;
import de.hsb.ai.util.ScotlandJadeHelper;

/**
 * ScotlandyardMasterAgent masters games between two players.
 */
@SuppressWarnings("serial")
public class ScotlandJadeMasterAgent extends AbstractAgent implements PlayerController {
	
	public static final String SERVICE_TYPE = "sy.master";
	
	private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(SERVICE_TYPE);
	
	protected PlayGame gameGUI;
	
	protected GameInfo gameInfo;
	
	private ScotlandJadeGameLogger gameLogger;

	private int remainingRuns = 1;
	
	public ScotlandJadeMasterAgent() {
		super(SERVICE_TYPE, SERVICE_TYPE);
	}
	
	@Override
	protected void setup() {
		super.setup();
		setupGUI();		
	}
	
	private Object mutex = new Object();

	private int round;
	
	private void sendAndWait(ACLMessage msg, long timeout) {
		try {
			send(msg);
			if (timeout > 0) {
				synchronized (mutex) {
//					logger.debug("waiting ... ");
					mutex.wait(timeout);
//					logger.debug("done");
				}
			}
		} catch (Exception e) {
			logger.error("waiting after message sent", e);
		}
	}
	
	private void setupGUI() {
		SwingUtilities.invokeLater(new Runnable() {
			
			@Override
			public void run() {
				try {
					gameGUI = new PlayGame(ScotlandJadeMasterAgent.this, false);
				} catch (IOException e) {
					logger.error("starting game gui failed", e);
					return;
				}
				gameGUI.addWindowListener(new WindowAdapter() {
					public void windowClosing(WindowEvent we) {
						System.exit(0);
					}
				});

				gameGUI.setSize(new Dimension(1057, 925));
				gameGUI.setVisible(true);
				gameGUI.setResizable(true);				
			}
		});
	}
	
	private ACLMessage createMoveRequest(AID receiver, List<Move> options) throws CodecException, OntologyException {
		ACLMessage msg = ScotlandJadeHelper.createDevilRequest(this, receiver, ACLMessage.REQUEST);
		
		GetMove gm = new GetMove();
		gm.setOptions(options);
		
		Action actionWrapper = new Action(receiver, gm);
		getContentManager().fillContent(msg, actionWrapper);
		return msg;
	}
	
	private ACLMessage createMoveEvent(int index, int from, Move move, Iterator<AID> receivers) throws CodecException, OntologyException {
		ACLMessage msg = ScotlandJadeHelper.createDevilRequest(this, receivers.next(), ACLMessage.INFORM);
		
		while (receivers.hasNext()) {
			msg.addReceiver(receivers.next());
		}
		
		MoveEvent m = new MoveEvent();
		m.setPlayer(gameInfo.getPlayers().get(index));
		m.setPlayerIndex(index);
		m.setFrom(from);
		m.setMove(move);
		m.setRound(round);
		
		Occurred occ = new Occurred();
		occ.setWhat(new EventRecord(m, null));
		
		getContentManager().fillContent(msg, occ);
		
		return msg;
	}
	
	private ACLMessage createGameOver(int playerIndex, int nodeIndex) throws CodecException, OntologyException {
		ACLMessage msg = ScotlandJadeHelper.createDevilRequest(this, gameInfo.getPlayers().get(0), ACLMessage.INFORM);
		
		for (int i = 1; i < gameInfo.getAllPlayers().size(); ++i) {
			msg.addReceiver(gameInfo.getPlayers().get(i));
		}
		
		AID winner = gameInfo.getPlayers().get(playerIndex);
		
		GameOver o = new GameOver();
		o.setWinner(winner);
		o.setMessage((playerIndex == 0 ? "Mr. X" : "Detective") + " [" +  winner.getLocalName() + "] wins");
		
		Occurred occ = new Occurred();
		occ.setWhat(new EventRecord(o, null));
		
		getContentManager().fillContent(msg, occ);
		
		return msg;
	}
	
	private ACLMessage createGameStartedEvent() throws CodecException, OntologyException {
		ACLMessage msg = ScotlandJadeHelper.createDevilRequest(this, gameInfo.getPlayers().get(0), ACLMessage.INFORM);
		
		for (int i = 1; i < gameInfo.getAllPlayers().size(); ++i) {
			msg.addReceiver(gameInfo.getPlayers().get(i));
		}
		
		GameStarted event = new GameStarted();
		event.setInfo(gameInfo);
		
		Occurred occ = new Occurred();
		occ.setWhat(new EventRecord(event, null));
		
		getContentManager().fillContent(msg, occ);
		
		return msg;
	}
	
	public void startGame(GameInfo info, int runs) {
		boolean changed = !info.equals(gameInfo);
		
		remainingRuns = runs;
		
		// Store game info
		gameInfo = info;
		
		int numDetectives = gameInfo.getAllPlayers().size() - 1;
		String fugitive = controls(0) ? gameInfo.getPlayers().get(0).getLocalName() : null;
		
		if (gameLogger == null || changed)
			gameLogger = ScotlandJadeHelper.createGameLogger(numDetectives, fugitive);
		
		gameLogger.open();
		
		try {
			// Propagate game started event
			sendAndWait(createGameStartedEvent(), ScotlandJadeHelper.config.getStartEventTimeout());
			
			logger.info("new game: " + gameInfo.getAllPlayers().size() + " players");
			
			// Start GUI-Game with N - 1 detectives where N = # of players
			gameGUI.startGame(numDetectives);
		} catch (Exception e) {
			logger.error("starting game", e);
		}
	
	}

	@Override
	public void getMove(int index, Collection<game.Move> options) {
		
		AID player = gameInfo.getPlayers().get(index);
		try {
			addBehaviour(new RequestMove(this, createMoveRequest(player, new ArrayList<Move>(options))));
		} catch (CodecException e) {
			e.printStackTrace();
		} catch (OntologyException e) {
			e.printStackTrace();
		}	
	}
	
	@Override
	public boolean controls(int index) {
		return index > 0 || !DummyAID.isDummy(gameInfo.getPlayers().get(0));
	}
	
	@Override
	public void notifyNewRound(int round) {
		this.round = round;
	}
	
	@Override
	public void notifyMove(int index, int from, Move move, boolean obfuscate) {
		try {
			logger.info("(" + round + ") move: [" + gameInfo.getPlayers().get(index).getLocalName() + "] from " + from + " -> " + move.toDisplayString());
			gameLogger.logMove(index, from, move);
			
			// Create a singleton iterator for fugitive
			Iterator<AID> playersIterator = Collections.singleton(gameInfo.getPlayers().get(0)).iterator();

			// Tell Fugitive the non-obfuscated move
			send(createMoveEvent(index, from, move, playersIterator));

			// Obfuscate move destination
			if (obfuscate) move.setNode(0);

			playersIterator = gameInfo.getPlayers().iterator();
			playersIterator.next(); // Skip first player (fugitive)
			
			// Tell detectives the obfuscated move
			sendAndWait(createMoveEvent(index, from, move, playersIterator), ScotlandJadeHelper.config.getMoveEventTimeout());
		} catch (Exception e) {
			logger.error("sending move event", e);
		}
	}
	
	@Override
	public void notifyWin(int playerIndex, int nodeIndex) {
		try {
			gameLogger.close();
			
			if (--remainingRuns > 0) {
				new Thread(new Runnable() {
					
					@Override
					public void run() {
						ScotlandJadeMasterAgent.this.startGame(gameInfo, remainingRuns);
					}
					
				}).start();
			}
			
			sendAndWait(createGameOver(playerIndex, nodeIndex), ScotlandJadeHelper.config.getMoveEventTimeout());
		} catch (Exception e) {
			logger.error("sending game over event", e);
		}
	}

	private class RequestMove extends SimpleAchieveREInitiator {
		
		public RequestMove(Agent a, ACLMessage msg) {
			super(a, msg);
		}
		
		@Override
		protected void handleInform(ACLMessage msg) {
			ACLMessage reply = null;
			try {
				ContentElement ce = getContentManager().extractContent(msg);
				if (ce instanceof Result) {
//					logger.debug("got result");
					reply = handleResult(msg, (Result)ce);
				} else {
					logger.warn("unexpected message");
				}
			} catch (Exception e) {
				logger.error("request delta failed", e);
				reply = msg.createReply();
				reply.setPerformative(ACLMessage.NOT_UNDERSTOOD);
			} finally {
				if (reply != null) {
					reset(reply);
				}
			}
		}
		
		@Override
		protected void handleAgree(ACLMessage msg) {
			logger.debug(msg.getSender().getLocalName() + " agreed");
		}
		
		@Override
		protected void handleRefuse(ACLMessage msg) {
			logger.warn(msg.getSender().getLocalName() + " refused");
		}
		
	}
	
	protected ACLMessage handleResult(ACLMessage msg, Result result) throws CodecException, OntologyException {
		ACLMessage rep	= msg.createReply();
		rep.setPerformative(ACLMessage.REFUSE);
		
		int index = 0;
		if (gameInfo != null) {
			Move value = (Move) result.getValue();
			for (int i = 0; i < gameInfo.getPlayers().size(); i++) {
				if(msg.getSender().equals(gameInfo.getAllPlayers().get(i))){
					index = i;
				}
			}
			gameGUI.setRecentMove(index, value);
		}
		
		return null;
	}

}
